// ============================================================================
// FileName: SIPSwitchProvisioningServices.cs
//
// Description:
// Provides the ability to add SIPAccounts, Extensions and Domains (administrators only) via
// a web service call.
//
// Author(s):
// Aaron Clauson
//
// History:
// 10 May 2008	Aaron Clauson	Created.
// ============================================================================

using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml;
using BlueFace.Sys;
using BlueFace.Sys.Net;
using BlueFace.Sys.WebServices;
using BlueFace.VoIP.App.SIP;
using BlueFace.VoIP.Net.SIP;
using BlueFace.VoIP.SIPServer;
using BlueFace.VoIP.SIPServerCores.StatefulProxy;
using log4net;

namespace SIPSwitch.Web
{
	[WebService(Namespace="http://www.mysipswitch.com")]
	public class SIPSwitchProvisioningServices : System.Web.Services.WebService
	{	
		private const int AUTHENTICATION_SESSION_TIMEOUT = 30;		// Sessions are valid for 30 minutes of inactivity.
        private const string UNKNOWN_ERROR_MESSAGE = "Sorry an unknown error has occurred processing your request. Please try reloading the page and if the error persists post a message on http://www.mysipswitch.com/forum/index.php.";
        private const string PUBLIC_SIP_DOMAIN = "sip.mysipswitch.com"; // Public domain that anybody can add SIP accounts for.
        public const int PASSWORD_MIN_LENGTH = 6;
        public const int PASSWORD_MAX_LENGTH = 15;
        public const int USERNAME_MIN_LENGTH = 5;
        public const int EXTENSION_LENGTH = 4;

        // Only non-printable non-alphanumeric ASCII characters missing are ; \ and space. The semi-colon isn't accepted by 
        // Netgears and the space has the potential to creat too much confusion wiht the users and \ with the system.
        public static readonly char[] NONAPLPHANUM_ALLOWED_PASSWORD_CHARS = new char[]{'!','"','$','%','&','(',')','*',
										   '+',',','.','/',':','<','=','>','?','@','[',']','^','_','`','{','|','}','~'};
        public static readonly string USERNAME_ALLOWED_CHARS = @"a-zA-Z0-9_\-\.";
        public static readonly string m_defaultDialPlanName = DialPlan.DEFAULT_DIALPLAN_NAME;

		private static ILog logger = AppState.GetLogger("provisioning");

        private static string m_switchDBConnStr = ConfigurationManager.AppSettings["SIPSwitchDBConnStr"];
        private static string m_switchStorageTypeStr = ConfigurationManager.AppSettings["SIPSwitchDBType"];
        private static string m_adminUsersPattern = ConfigurationManager.AppSettings["AdminUsersPattern"];  // Users that will be identified as admins.
        
        private StorageTypes m_sipSwitchStorageType;
        private StorageLayer m_sipSwitchStorageLayer = null;
        private SIPAssetPersistor<SIPAccount> m_sipAccountPersistor;
        private SIPProxyWebServiceClient m_proxyInfoServiceProxy = new SIPProxyWebServiceClient();

		public SIPSwitchProvisioningServices()
		{
            try
            {
                m_sipSwitchStorageType = StorageTypesConverter.GetStorageType(m_switchStorageTypeStr);
                m_sipSwitchStorageLayer = new StorageLayer(m_sipSwitchStorageType, m_switchDBConnStr);
                m_sipAccountPersistor = SIPAssetPersistorFactory.CreateSIPAccountPersistor(m_sipSwitchStorageType, m_switchDBConnStr);
            }
            catch (Exception excp)
            {
                logger.Error("Exception SIPSwitchProvisioningServices (ctor). " + excp.Message);
                throw excp;
            }
		}
		
		[WebMethod]
		public bool IsAlive()
		{
			try
			{
                logger.Debug("SIPSwitchProvisioningServices IsAlive called.");
				return true;
			}
			catch(Exception excp)
			{
				logger.Error("Exception IsAlive. " + excp.Message);
				throw excp;
			}
		}

		[WebMethod]
		public string Echo(string message)
		{
            logger.Debug("SIPSwitchProvisioningServices Echo called with message of: " + message);
			return message;
		}

        [WebMethod]
        public List<SIPAccount> GetSIPAccounts(string username, string password)
        {
            try
            {
                if (Authenticate(username, password))
                {
                    return m_sipAccountPersistor.Get(username);
                }
                else
                {
                    throw new UnauthorizedAccessException();
                }
            }
            catch(Exception excp)
            {
                logger.Error("Exception GetSIPAccounts. " + excp);
                throw excp;
            }
        }

        [WebMethod]
        public List<string> GetDomains(string username, string password)
        {
            try
            {
                if (Authenticate(username, password))
                {
                    List<string> domainList = new List<string>();

                    string selectSQL = "select domain from domains where admingroupid in (select admingroupid from admingroupmembers where username = '" + username.Replace("'", "''") + "')";
                    DataSet domainSet = m_sipSwitchStorageLayer.GetDataSet(selectSQL);

                    foreach (DataRow domainRow in domainSet.Tables[0].Rows)
                    {
                        domainList.Add(domainRow["domain"] as string);
                    }

                    return domainList;
                }
                else
                {
                    throw new UnauthorizedAccessException();
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception GetDomains. " + excp);
                throw excp;
            }
        }

        [WebMethod]
        public List<string> GetExtensions(string username, string password)
        {
            try
            {
                if (Authenticate(username, password))
                {
                    List<string> extensionsList = new List<string>();

                    string selectSQL = "select extension, domain from extensions ex join domains da on ex.domainid = da.domainid where owner = '" + username.Replace("'", "''") + "'";
                    DataSet extensionSet = m_sipSwitchStorageLayer.GetDataSet(selectSQL);

                    foreach (DataRow extensionRow in extensionSet.Tables[0].Rows)
                    {
                        extensionsList.Add(extensionRow["extension"] as string + "@" + extensionRow["domain"] as string);
                    }

                    return extensionsList;
                }
                else
                {
                    throw new UnauthorizedAccessException();
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception GetExtensions. " + excp);
                throw excp;
            }
        }

        [WebMethod]
        public void AddDomain(string username, string password, string owner, string domain)
        {
            try
            {
                if (Authenticate(username, password))
                {
                    if (Regex.Match(username, m_adminUsersPattern).Success)
                    {
                        // Get the admin group for this user or create a new one if it doesn't exist.
                        string selectAdminGroupId = "select admingroupid from admingroupmembers where username = '" + owner.Replace("'", "''") + "'";
                        string adminGroupId = m_sipSwitchStorageLayer.ExecuteScalar(selectAdminGroupId) as string;

                        if(adminGroupId == null)
                        {
                            adminGroupId = Guid.NewGuid().ToString();
                            string insertAdminGrouopSQL = "insert into admingroups (admingroupid, admingroupname) values ('" + adminGroupId + "', '" +  owner.Replace("'", "''") + "')";
                            m_sipSwitchStorageLayer.ExecuteNonQuery(insertAdminGrouopSQL);

                            string insertMemberSQL = "insert into admingroupmembers (admingroupmemberid, admingroupid, username) values ('" + Guid.NewGuid() + "', '" + adminGroupId + "', '" + owner.Replace("'", "''") + "')";
                            m_sipSwitchStorageLayer.ExecuteNonQuery(insertMemberSQL);
                        }

                        string insertSQL = "insert into domains (domainid, domain, admingroupid) values ('" + Guid.NewGuid() + "', '" + domain.Trim().Replace("'", "''").ToLower() + "', '" + adminGroupId + "')";
                        m_sipSwitchStorageLayer.ExecuteNonQuery(insertSQL);

                        m_proxyInfoServiceProxy.AddRegistrarDomain(domain);
                    }
                    else
                    {
                        throw new UnauthorizedAccessException("Please contact a sipswitch administrator if you would like to add a domain you own.");
                    }
                }
                else
                {
                    throw new UnauthorizedAccessException();
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception AddDomain. " + excp);
                throw excp;
            }
        }

        [WebMethod]
        public void AddDomainAlias(string username, string password, string domain, string alias)
        {
            try
            {
                if (Authenticate(username, password))
                {
                    if (Regex.Match(username, m_adminUsersPattern).Success)
                    {
                        string domainId = m_sipSwitchStorageLayer.ExecuteScalar("select domainid from domains where domain = '" + domain.Replace("'", "''") + "'") as string;

                        if (domainId != null)
                        {
                            string insertSQL = "insert into domainaliases (domainaliasid, domainid, domainalias) values ('" + Guid.NewGuid() + "', '" + domainId + "', '" + alias.Trim().Replace("'", "''").ToLower() + "')";
                            m_sipSwitchStorageLayer.ExecuteNonQuery(insertSQL);


                            m_proxyInfoServiceProxy.AddRegistrarDomainAlias(domain, alias);
                        }
                        else
                        {
                            throw new ApplicationException("Could not find domain " + domain + " to add an alias for.");
                        }
                    }
                    else
                    {
                        throw new UnauthorizedAccessException("Please contact a sipswitch administrator if you would like to add a domain alias.");
                    }
                }
                else
                {
                    throw new UnauthorizedAccessException();
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception AddDomainAlias. " + excp);
                throw excp;
            }
        }

        [WebMethod]
        public void AddSIPAccount(string username, string password, string sipDomain, string sipUsername, string sipPassword)
        {
            try
            {
                if (Authenticate(username, password))
                {
                    if (sipDomain == null || sipDomain.Trim().Length == 0)
                    {
                        sipDomain = PUBLIC_SIP_DOMAIN;
                    }

                    if (sipDomain == PUBLIC_SIP_DOMAIN || Regex.Match(username, m_adminUsersPattern).Success || IsDomainOwner(username, sipDomain))
                    {
                        if (sipUsername == null || sipUsername.Trim().Length < USERNAME_MIN_LENGTH)
                        {
                            throw new ArgumentException("The username provided was empty or too short. It must be at least " + USERNAME_MIN_LENGTH + " characters long.");
                        }
                        else if (Regex.Match(sipUsername, "[^" + USERNAME_ALLOWED_CHARS + "]").Success)
                        {
                            throw new ArgumentException("The username had an invalid character, characters permitted are alpha-numreic and .-_.");
                        }
                        else if (sipPassword == null || sipPassword.Trim().Length == 0)
                        {
                            throw new ArgumentException("A password must be specified.");
                        }
                        else if (sipPassword.Length < PASSWORD_MIN_LENGTH || sipPassword.Length > PASSWORD_MAX_LENGTH)
                        {
                            throw new ArgumentException("The Password field must be at least " + PASSWORD_MIN_LENGTH + " characters and no more than " + PASSWORD_MAX_LENGTH + " characters.");
                        }
                        else
                        {
                            #region Check the password illegal characters.

                            char[] passwordChars = sipPassword.ToCharArray();

                            bool illegalCharFound = false;
                            char illegalChar = ' ';

                            foreach (char passwordChar in passwordChars)
                            {
                                if (Regex.Match(passwordChar.ToString(), "[a-zA-Z0-9]").Success)
                                {
                                    continue;
                                }
                                else
                                {
                                    bool validChar = false;
                                    foreach (char allowedChar in NONAPLPHANUM_ALLOWED_PASSWORD_CHARS)
                                    {
                                        if (allowedChar == passwordChar)
                                        {
                                            validChar = true;
                                            break;
                                        }
                                    }

                                    if (validChar)
                                    {
                                        continue;
                                    }
                                    else
                                    {
                                        illegalCharFound = true;
                                        illegalChar = passwordChar;
                                        break;
                                    }
                                }
                            }

                            #endregion

                            if (illegalCharFound)
                            {
                                throw new ArgumentException("Your password has an invalid character " + illegalChar + " it can only contain a to Z, 0 to 9 and characters in this set " + SafeXML.MakeSafeXML(new String(NONAPLPHANUM_ALLOWED_PASSWORD_CHARS)) + ".");
                            }
                        }

                        string extensionExistsSQL = "select count(*) from extensions where extension = '" + sipUsername.Replace("'", "''") + "' and domainid = (select domainid from domains where domain = '" + sipDomain.Replace("'", "''") + "')";
                        bool extensionExists = Convert.ToInt32(m_sipSwitchStorageLayer.ExecuteScalar(extensionExistsSQL)) > 0;

                        // Also check whether there is a SIP account with the same name which would cause a conflict.
                        string sipAccountExistsSQL = "select count(*) from sipaccounts where sipusername = '" + sipUsername.Replace("'", "''") + "' and domainid = (select domainid from domains where domain = '" + sipDomain.Replace("'", "''") + "')";
                        bool sipAccountExists = Convert.ToInt32(m_sipSwitchStorageLayer.ExecuteScalar(sipAccountExistsSQL)) > 0;

                        if (extensionExists || sipAccountExists)
                        {
                            throw new ApplicationException("SIP account " + sipUsername + "@" + sipDomain + " is already allocated.");
                        }
                        else
                        {
                            SIPAccount sipAccount = new SIPAccount(username, sipUsername.Trim(), sipPassword.Trim(), sipDomain.Trim(), m_defaultDialPlanName);
                            m_sipAccountPersistor.Update(sipAccount);
                        }
                    }
                    else
                    {
                        throw new UnauthorizedAccessException("Sorry you are not an administrator for " + sipDomain + ". The public domain is " + PUBLIC_SIP_DOMAIN + ".");
                    }
                }
                else
                {
                    throw new UnauthorizedAccessException();
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception AdSIPAccount. " + excp);
                throw excp;
            }
        }

        [WebMethod]
        public void DeleteSIPAccount(string username, string password, string sipDomain, string sipUsername)
        {
            try
            {
                if (Authenticate(username, password))
                {
                    bool deleteAuthorised = Regex.Match(username, m_adminUsersPattern).Success;

                    SIPAccount sipAccount = m_sipAccountPersistor.Get(sipUsername, sipDomain);

                    if (sipAccount == null)
                    {
                        // Don't give any extra information to avoid this method being used to discover SIP account usernames.
                        throw new UnauthorizedAccessException();
                    }
                    else
                    {
                        if (Regex.Match(username, m_adminUsersPattern).Success || username == sipAccount.Owner)
                        {
                            m_sipAccountPersistor.Delete(sipAccount);
                        }
                        else
                        {
                            // Don't give any extra information to avoid this method being used to discover SIP account usernames.
                            throw new UnauthorizedAccessException();
                        }
                    }
                }
                else
                {
                    throw new UnauthorizedAccessException();
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception DeleteSIPAccount. " + excp);
                throw excp;
            }
        }

        [WebMethod]
        public void AllocateExtension(string username, string password, string extension, string domain)
        {
            try
            {
                if (Authenticate(username, password))
                {
                    if (domain == null || domain.Trim().Length == 0)
                    {
                        domain = PUBLIC_SIP_DOMAIN;
                    }

                    if (domain == PUBLIC_SIP_DOMAIN || Regex.Match(username, m_adminUsersPattern).Success || IsDomainOwner(username, domain))
                    {
                        if (extension == null || Regex.Match(extension, "[^" + USERNAME_ALLOWED_CHARS + "]").Success || extension.Trim().Length < EXTENSION_LENGTH)
                        {
                            throw new ArgumentException("The extension provided was empty, had a non permitted character or was less than the required length of " + EXTENSION_LENGTH + " characters.");
                        }

                        string extensionExistsSQL = "select count(*) from extensions where extension = '" + extension + "' and domainid = (select domainid from domains where domain = '" + domain.Replace("'", "''") + "')";
                        bool extensionExists = Convert.ToInt32(m_sipSwitchStorageLayer.ExecuteScalar(extensionExistsSQL)) > 0;

                        // Also check whether there is a SIP account with the same name which would cause a conflict.
                        string sipAccountExistsSQL = "select count(*) from sipaccounts where sipusername = '" + extension + "' and domainid = (select domainid from domains where domain = '" + domain.Replace("'", "''") + "')";
                        bool sipAccountExists = Convert.ToInt32(m_sipSwitchStorageLayer.ExecuteScalar(sipAccountExistsSQL)) > 0;

                        if (extensionExists || sipAccountExists)
                        {
                            throw new ApplicationException("Extension " + extension + "@" + domain + " is already allocated.");
                        }
                        else
                        {
                            string selectDomainId = "select domainid from domains where domain = '" + domain.Replace("'", "''") + "'";
                            string domainId = m_sipSwitchStorageLayer.ExecuteScalar(selectDomainId) as string;

                            string insertSQL = "insert into extensions (extensionid, extension, domainid, owner) values ('" + Guid.NewGuid() + "', '" + extension.Replace("'", "''") + "', '" + domainId + "', '" + username.Replace("'", "''") + "')";
                            m_sipSwitchStorageLayer.ExecuteNonQuery(insertSQL);
                        }
                    }
                    else
                    {
                        throw new UnauthorizedAccessException("Sorry you are not an administrator for " + domain + ". The public domain is " + PUBLIC_SIP_DOMAIN + ".");
                    }
                }
                else
                {
                    throw new UnauthorizedAccessException();
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception AllocateExtension. " + excp);
                throw excp;
            }
        }

        [WebMethod]
        public void DeallocateExtension(string username, string password, string extension, string domain)
        {
            try
            {
                if (Authenticate(username, password))
                {
                    bool isAuthorised = Regex.Match(username, m_adminUsersPattern).Success;

                    if (!isAuthorised)
                    {
                        string extensionOwner = m_sipSwitchStorageLayer.ExecuteScalar("select owner from extensions where extension = '" + extension.Replace("'", "''") + "' and domainid = (select domainid from domains where domain = '" + domain.Replace("'", "''") + "')") as string;
                        if (extensionOwner == username)
                        {
                            isAuthorised = true;
                        }
                    }

                    if (!isAuthorised)
                    {
                        throw new UnauthorizedAccessException("Sorry you are not the owner of this extension.");
                    }
                    else
                    {
                        string selectDomainId = "select domainid from domains where domain = '" + domain.Replace("'", "''") + "'";
                        string domainId = m_sipSwitchStorageLayer.ExecuteScalar(selectDomainId) as string;

                        m_sipSwitchStorageLayer.ExecuteNonQuery("delete from extensions where extension = '" + extension.Replace("'", "''") + "' and domainid = '" + domainId + "'");
                    }
                }
                else
                {
                    throw new UnauthorizedAccessException();
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception DeallocateExtension. " + excp);
                throw excp;
            }
        }

        [WebMethod]
        public void RemoveDomain(string username, string password, string domain)
        {
            try
            {
                if (Authenticate(username, password))
                {
                    if (Regex.Match(username, m_adminUsersPattern).Success)
                    {
                        m_sipSwitchStorageLayer.ExecuteNonQuery("delete from domainaliases where domainalias = '" + domain.Replace("'", "''") + "'");
                        m_sipSwitchStorageLayer.ExecuteNonQuery("delete from domains where domain = '" + domain.Replace("'", "''") + "'");

                        m_proxyInfoServiceProxy.RemoveRegistrarDomain(domain);
                    }
                    else
                    {
                        throw new UnauthorizedAccessException("Please contact a sipswitch administrator if you would like to remove a domain/alias you own.");
                    }
                }
                else
                {
                    throw new UnauthorizedAccessException();
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception RemovedDomain. " + excp);
                throw excp;
            }
        }

		private bool Authenticate(string username, string password)
		{
            try
            {
                string selectSQL = "select count(*) from customers where username = '" + username.Replace("'", "''") + "' and password = '" + password.Replace("'", "''") + "' and suspended <> '1'";
                return Convert.ToInt32(m_sipSwitchStorageLayer.ExecuteScalar(selectSQL)) > 0;
            }
            catch (Exception excp)
            {
                logger.Error("Exception Authenticate. " + excp.Message);
                return false;
            }
		}

        private bool IsDomainOwner(string username, string domain)
        {
             string selectSQL = 
                "select count(*) from domains where " +
                " domain = '" + domain.Replace("'", "''") + "' and " +
                " admingroupid in (select admingroupid from admingroupmembers where username = '" + username.Replace("'", "''") + "')";

            return Convert.ToInt32(m_sipSwitchStorageLayer.ExecuteScalar(selectSQL)) > 0;
        }
	}
}
